﻿using Futaba.SBus.Telemetry.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Futaba.SBus.Extensions;

namespace Futaba.SBus.Telemetry
{

	/// <summary>
	/// Produces <see cref="Slot">Slots</see> from a FLI file
	/// </summary>
	/// <remarks>
	/// FLI files are produced by the Futaba Transmitters and saved to the SDCard
	/// along with the FLD data-file. They hold the sensor/slot information configured
	/// in the transmitter so all systems know how to interpret the telemetry-data saved
	/// </remarks>
	public class SlotFactory
	{
		SensorFactory _sensorFactory;

		#region Singleton

		private static SlotFactory _singletonSlotFactory;
		private static readonly object SlotFactorySingletonLock = new object();

		/// <summary>
		/// A singleton of this class
		/// </summary>
		public static SlotFactory DefaultInstance
		{
			get
			{
				if (_singletonSlotFactory == null)
				{
					// ReSharper disable PossibleMultipleWriteAccessInDoubleCheckLocking
					lock (SlotFactorySingletonLock)
					{
						if (_singletonSlotFactory == null)
							_singletonSlotFactory = new SlotFactory();
					}
					// ReSharper restore PossibleMultipleWriteAccessInDoubleCheckLocking
				}
				return _singletonSlotFactory;
			}
		}

		#endregion

		/// <summary>
		/// An optional factory method which creates a new instance of a
		/// <see cref="ISlot"/>
		/// </summary>
		public Func<ISlot> CreateSlot { get; set; }

		/// <summary>
		/// Generates the default instance
		/// </summary>
		public SlotFactory()
		{
		}

		/// <summary>
		/// Generates an instance with a <see cref="SensorFactory"/> provided
		/// </summary>
		public SlotFactory(SensorFactory sensorFactory)
		{
			sensorFactory.EnsureNotNull("sensorFactory");
			_sensorFactory = sensorFactory;
		}

		private const byte Sbus2MaxSlots = 32;

		/// <summary>
		/// Parses the supplied stream and returns the slot-definitions. 
		/// It uses the sensor-definitions supplied by the <see cref="SensorFactory"/>
		/// supplied in the CTOR or the default factory, if none was supplied
		/// </summary>
		/// <param name="fliFile">the file to parse</param>
		/// <returns>A list of all detected/configured <see cref="Slot">slots</see></returns>
		public ISlot[] ParseFliFile(Stream fliFile)
		{
			var sensorFactory = GetSensorFactory();
			return ParseFliFile(fliFile, sensorFactory.GetKnownSensors());
		}

		/// <summary>
		/// Parses the supplied stream and returns the slot-definitions. 
		/// It uses the supplied sensor-definitions.
		/// </summary>
		/// <param name="fliFile">the file to parse</param>
		/// <param name="knownSensors">a list of <see cref="Sensor"/>-configurations</param>
		/// <returns>A list of all detected/configured <see cref="Slot">slots</see></returns>
		public ISlot[] ParseFliFile(Stream fliFile, IEnumerable<Sensor> knownSensors)
		{
			knownSensors.EnsureNotEmpty("knownSensors");

			var q = ReadConfiguration(fliFile);
			var slots = ParseConfigurationAndGenerateSlots(knownSensors, q);
			return slots.ToArray();
		}

		/// <summary>
		/// Loads and returns the list of <see cref="Slot">slots</see> from the provided configuration file
		/// </summary>
		/// <param name="xmlConfiguration">the file to load the slots from</param>
		/// <param name="knownSensors">a list of <see cref="Sensor"/>-configurations</param>
		/// <seealso cref="Save"/>
		public Slot[] Load(Stream xmlConfiguration, Sensor[] knownSensors = null)
		{
			var slots = xmlConfiguration.Load<Slot[]>(null);
			ConnectSlotToConfiguration(slots, knownSensors);
			return slots;
		}

		/// <summary>
		/// Assigns each <see cref="Slot"/> the corresponding <see cref="Sensor"/>-configuration
		/// </summary>
		/// <remarks>
		/// The <see cref="Slot.Sensor">Sensor</see> of each slot is checked and set (if null) with the
		/// corresponding <paramref name="knownSensors"/>
		/// </remarks>
		/// <param name="slots">the slots to process</param>
		/// <param name="knownSensors">a list of <see cref="Sensor"/>-configurations</param>
		/// <exception cref="ArgumentException">thrown, if a corresponding sensor was not provided for a slot</exception>
		public void ConnectSlotToConfiguration(ISlot[] slots, Sensor[] knownSensors = null)
		{
			if (knownSensors == null)
				knownSensors = GetSensorFactory().GetKnownSensors();

			foreach (var slot in slots.Where(s => s.Sensor == null))
			{
				if (knownSensors.Where(s => s.SensorType == slot.SensorType).Count() != 1)
					throw new ArgumentException(string.Format("No sensor of type {0} has been supplied", slot.SensorType));

				var sensor = knownSensors.Single(s => s.SensorType == slot.SensorType);
				slot.Sensor = sensor;
			}

			foreach (var slot in slots.Where(s => s.Parameter == null))
			{
				if (slot.Sensor.Parameters.Where(p => p.ParameterName == slot.ParameterName).Count() != 1)
					throw new ArgumentException(string.Format("No parameter with name {0} for sensor {1} has been supplied",
						slot.ParameterName, slot.SensorType));

				var parameter = slot.Sensor.Parameters.Where(p => p.ParameterName == slot.ParameterName).Single();
				slot.Parameter = parameter;
			}
		}

		/// <summary>
		/// Saves the supplied <see cref="Slot">slots</see> to the provided stream as XML
		/// </summary>
		/// <param name="slots">the slots to save</param>
		/// <param name="outFile">the stream to write the XML to</param>
		/// <param name="encoding">the optional encoding to use for XML serialization.
		/// If not supplied, <see cref="Encoding.Default"/> is used instead</param>
		public void Save(Slot[] slots, Stream outFile, Encoding encoding = null)
		{
			slots.ToXml(outFile, encoding ?? Encoding.Default, null);
		}

		/// <summary>
		/// Returns a default <see cref="SensorFactory"/>
		/// </summary>
		/// <remarks>
		/// if set, returns the <see cref="SensorFactory"/> provided to the CTOR.
		/// Otherwise generates a default instance and returns it on each subsequent call
		/// </remarks>
		/// <returns></returns>
		protected virtual SensorFactory GetSensorFactory()
		{
			return _sensorFactory ?? SensorFactory.DefaultInstance;
		}

		private Queue<byte> ReadConfiguration(Stream fliFile)
		{
			/*
			 * All slots consume 2 bytes (1=sensor type, 2=slotId)
			 * Sensor-Type RECEIVER consumes 3 Slots (6 bytes)
			 * Slots 1-31 consume 62 bytes
			 * 2 end-bytes 0x0 mark end
			 * */
			const int messageSize = ((Sbus2MaxSlots + 2) * 2) + 2;

			var data = new byte[messageSize];
			var read = fliFile.Read(data, 0, messageSize);
			if (read != messageSize)
				throw new InvalidOperationException(string.Format("Could not read {0} bytes from stream. Really a Futaba SBUS2 FLI file?", messageSize));

			if (data[messageSize - 2] != 0x0 || data[messageSize - 1] != 0x0)
				throw new InvalidOperationException("Message does not end in 0x0, 0x0");
			var q = new Queue<byte>(data.Take(messageSize - 2));
			return q;
		}

		private List<ISlot> ParseConfigurationAndGenerateSlots(IEnumerable<Sensor> knownSensors, Queue<byte> q)
		{
			var slots = new List<ISlot>(Sbus2MaxSlots);
			while (q.Count > 0)
			{
				var sensorFliId = q.Dequeue();
				var slotId = q.Dequeue();

				if (!Enum.IsDefined(typeof(SensorType), sensorFliId))
					throw new NotSupportedException(string.Format("Unknown Sensor Type {0} for slot {1}", sensorFliId, slotId));

				var sensorType = (SensorType)sensorFliId;
				if (sensorType == SensorType.Receiver && slots.Any(s => s.SensorType == SensorType.Receiver))
					continue; //subsequent 0x0 means unassigned slot, not another receiver

#if DEBUG
				System.Diagnostics.Debug.WriteLine("Sensor {0} at slot {1}", sensorType, slotId);
#endif

				var sensor = knownSensors.SingleOrDefault(s => s.SensorType == sensorType);
				var generatedSlots = GenerateFromSensor(sensor, slotId);
				slots.AddRange(generatedSlots);

				SkipAdditionalConsumedSlotsOfSensor(q, generatedSlots);
			}
			return slots;
		}

		private void SkipAdditionalConsumedSlotsOfSensor(Queue<byte> q, ISlot[] generatedSlots)
		{
			for (var i = 0; i < (generatedSlots.Sum(g => g.Parameter.ConsumesSlots) - 1) * 2; i++)
				q.Dequeue();
		}

		private ISlot[] GenerateFromSensor(Sensor sensor, int firstSlot)
		{
			var slots = new List<ISlot>();
			foreach (var p in sensor.Parameters)
			{
				int curSlot = firstSlot + (p.Position - 1);
#if DEBUG
				System.Diagnostics.Debug.WriteLine("  Parameter {0} >> slot {1}", p, curSlot);
#endif

				var s = CreateSlot != null ? CreateSlot() : new Slot();

				s.DisplayName = p.DisplayName;
				s.Sensor = sensor;
				s.Parameter = p;
				s.ValueMultiplyer = p.ValueMultiplyer;
				s.SlotId = Slot.GetId(curSlot);

				slots.Add(s);
				curSlot += p.ConsumesSlots;
			}
			return slots.ToArray();
		}

	}
}
